from tkinter import *
from tkinter.ttk import *
import tkinter.messagebox
import time
from concurrent.futures import ThreadPoolExecutor
import threading
from abc import ABC, abstractmethod
from loggers import dev_logger, prod_logger, record_logger
import collections
import json
import re
import pysnooper

UnitTestDataStructure = collections.namedtuple(
    "UnitTestDataStructure", ["name", "send", "rcev"]
)


class TestDataStucture:
    """
    每个功能模块测试数据,单元测试用具名元组展示出名称,发送数据,接收数据
    """

    def __init__(
        self, PosId="XY", MsgId=0xFFF, rcevId=0xFFF, name="ModelName", testDatas=None
    ) -> None:
        # TODO: 后面用**kwargs直接拆包方式
        self.PosId = PosId
        self.MsgId = MsgId
        self.rcevId = rcevId
        self.name = name
        self.testDatas = []

        if testDatas is not None:
            for utData in testDatas:
                cur = UnitTestDataStructure(utData[0], utData[1], utData[2])
                self.testDatas.append(cur)

    def toDict(self):
        dictData = {
            "PosId": self.PosId,
            "MsgId": self.MsgId,
            "rcevId": self.rcevId,
            "name": self.name,
            "testDatas": self.testDatas,
        }
        return dictData


class FunctestBase(ABC, Toplevel):
    """
    基础抽象类
    """
    # 设定只有一个所有线程共用的线程的线程池，避免多线程交叉执行导致的数据异常
    # 每个单元测试下面的每个key代表的功能测试所用的线程池
    testSinglePoolUnitKey = ThreadPoolExecutor(max_workers=1)
    # 单元测试allTest所用的线程池
    testSinglePoolUnit = ThreadPoolExecutor(max_workers=1)
    _objDict = {}

    def __new__(cls, *args, **kwargs):
        PosId = kwargs.get('PosId')
        if PosId not in cls._objDict:
            newObj = object.__new__(cls)
            return newObj
        else:
            cls._objDict[PosId].resetTestState()
            cls._objDict[PosId].deiconify()
            return cls._objDict[PosId]
        
    def resetTestState(self):
        # 将上一次测试的展示状态,重置
        for key, data in enumerate(self.testDatas):
            self.sendDataLabelDict[key].configure(text='sendstate', background='gray')
            self.recvDataLabelDict[key].configure(text='recvstate', background='gray')
            self.alltestResMsg.config(foreground='gray',text='批量测试结果')

    def __init__(
        self, root, PosId="xy", name="XXXX模块demo", MsgId=0xFFF,rcevId=0x999, testDatas=None
    ) -> None:
        # 如果之前已经实例化过，就不在进行初始化。
        # 否则就进行后面的初始化，并且在最后把该示例放到示例的字典_objDict中
        if PosId in self._objDict:
            return
        else:
            self._objDict[PosId] = self

        """
        root 是主窗口的实例self
        """
        super().__init__()
        self.master = root
        # 给父窗口设定个醒目的变量名
        self.MASTER = self.master
        self.title(f"测试控制面板----{name}-{PosId}")

        self.name = name
        self.PosId = PosId
        # 设定id值的随时跟踪变化
        self.MsgId = MsgId
        self.idIntVar = IntVar()
        self.idIntVar.set(self.MsgId)
        # 设定反馈报文的id号
        self.rcevId = rcevId
        self.idRcevIntvar = IntVar()
        self.idRcevIntvar.set(self.rcevId)

        # 设定输入值数据,没有输入用默认值代替
        if testDatas is not None:
            self.testDatas = testDatas
        else:
            testDatas = [
                UnitTestDataStructure("name1", "senddata1", "recvdata1"),
                UnitTestDataStructure("name2", "senddata2", "recvdata2"),
                UnitTestDataStructure("name3", "senddata3", "recvdata3"),
                UnitTestDataStructure("name4", "senddata4", "recvdata4"),
                UnitTestDataStructure("name5", "senddata5", "recvdata5"),
                UnitTestDataStructure("name6", "senddata6", "recvdata6"),
            ]

        # 前面设定完了基础值,然后再做gui的初始化
        self.viewInit()
        self.protocol('WM_DELETE_WINDOW', self.closeHandle)


    def closeHandle(self):
        # 把关闭窗口的destroy改为withdraw,避免关闭窗口后的在开启报错.
        # tkinter.messagebox.showwarning('关闭窗口', '您将关闭窗口self...')
        self.withdraw()

    @abstractmethod
    def viewInit(self, *args, **kwargs):
        pass

    @abstractmethod
    def sendControlMsg(self, *args, **kwargs):
        pass


class FunctestTemplate(FunctestBase):
    def initData(self):
        """
        TODO: 给570 和 280 留着该方法,后续优化掉
        """
        pass

    def createDict(self):
        self.initData()

    def validateMsgData(self, event):
        thisEntry = event.widget
        content = thisEntry.get()
        pattern = re.compile(r"^[0-9a-fA-F]{16}$")
        if pattern.match(content):
            thisEntry.configure(background="green")
        else:
            thisEntry.configure(background="yellow")

    def viewInit(self):
        self.createDict()
        dev_logger.debug(self.testDatas)
        self.opFrame = Frame(self, borderwidth=2, relief="raise")
        self.opFrame.grid(row=1, column=1)
        self.opFrameSub1 = Frame(self.opFrame, borderwidth=1, relief="groove")
        self.opFrameSub1.grid(row=1, column=0)
        self.alltestBtn = Button(
            self.opFrameSub1,
            text="批量测试",
            # TODO: 字体更改
            # font=("Arail", 16),
            command=self.allTest,
        )
        self.alltestBtn.grid(row=0, column=0)
        self.alltestResMsg = Message(self.opFrameSub1,width=200,foreground='gray',text='批量测试结果')
        self.alltestResMsg.grid(row=0, column=1)

        if 1:
            self.opFrameSub2 = Frame(self.opFrame, borderwidth=1, relief="groove")
            self.opFrameSub2.grid(row=2, column=0)

            self.sendDataEntryDict = {}
            self.sendDataBtnDict = {}
            self.sendDataLabelDict = {}
            self.recvDataLabelDict = {}
            self.sendDataStringDict = {}
            self.rcevDataStringDict = {}
            self.rcevDataEntryDict = {}

            # 用一个代用key的字典,通过for循环,把每个测试数据与设定的控件关联起来
            self.testDataDict = {}
            for key, data in enumerate(self.testDatas):
                # 发送数据设定
                self.testDataDict[key] = data
                self.sendDataStringDict[key] = StringVar()
                self.sendDataStringDict[key].set(data.send)
                self.sendDataEntryDict[key] = Entry(
                    self.opFrameSub2,
                    width=20,
                    state="disabled",
                    textvariable=self.sendDataStringDict[key],
                )
                self.sendDataEntryDict[key].grid(row=key, column=0, pady=5, padx=20)
                self.sendDataEntryDict[key].bind("<KeyRelease>", self.validateMsgData)

                self.sendDataBtnDict[key] = Button(
                    self.opFrameSub2,
                    text=data.name,
                    command=self.sendControlMsg(key),
                )
                self.sendDataBtnDict[key].grid(row=key, column=1, pady=5, padx=20)

                self.sendDataLabelDict[key] = Label(self.opFrameSub2, text="sendstate")
                self.sendDataLabelDict[key].grid(row=key, column=2, pady=5, padx=20)

                self.recvDataLabelDict[key] = Label(self.opFrameSub2, text="rcevstate")
                self.recvDataLabelDict[key].grid(row=key, column=3, pady=5, padx=20)

                self.rcevDataStringDict[key] = StringVar()
                self.rcevDataStringDict[key].set(data.rcev)
                self.rcevDataEntryDict[key] = Entry(
                    self.opFrameSub2,
                    width=20,
                    state="disabled",
                    textvariable=self.rcevDataStringDict[key],
                )
                self.rcevDataEntryDict[key].grid(row=key, column=4, pady=5, padx=20)
                self.rcevDataEntryDict[key].bind("<KeyRelease>", self.validateMsgData)

        self.configFrame = Frame(self, borderwidth=2, relief="raise")
        self.configFrame.grid(row=2, column=1)
        self.enableEntrysBtn = Button(
            self.configFrame, text="输入框开启", command=self.enableEntrysBtnCmd
        )
        self.enableEntrysBtn.pack(side="left")
        self.disableEntrysBtn = Button(
            self.configFrame, text="输入框屏蔽", command=self.disableEntrysBtnCmd
        )
        self.disableEntrysBtn.pack(side="left")
        self.configBtn = Button(
            self.configFrame, text="设置测试全局参数", command=self.initConfigPanel
        ).pack(side="left")

    def enableEntrysBtnCmd(self):
        for _, entry in self.sendDataEntryDict.items():
            entry.configure(state="normal")
        for _, entry in self.rcevDataEntryDict.items():
            entry.configure(state="normal")

    def disableEntrysBtnCmd(self):
        for _, entry in self.sendDataEntryDict.items():
            entry.configure(state="disabled")
        for _, entry in self.rcevDataEntryDict.items():
            entry.configure(state="disabled")

    def initConfigPanel(self):
        self.configPanel = Toplevel(self)
        self.configPanel.title(f"{self.name}测试参数设置")
        self.configPanelFr1 = Frame(self.configPanel)
        self.configPanelFr1.grid(row=1, column=1)
        Label(self.configPanelFr1, text="预留该页面用于设定通用数据").grid(
            row=1, columnspan=2, column=1
        )

        self.configPanelIdLabel = Label(self.configPanelFr1, text="模块MsgId设置")
        self.configPanelIdLabel.grid(row=2, column=1)
        self.idEntry = Entry(self.configPanelFr1, textvariable=self.idIntVar)
        self.idEntry.grid(row=2, column=2)

        self.configPanelRcevIdLabel = Label(self.configPanelFr1, text="反馈报文Id设置")
        self.configPanelRcevIdLabel.grid(row=3, column=1)
        self.idEntry1 = Entry(self.configPanelFr1, textvariable=self.idRcevIntvar)
        self.idEntry1.grid(row=3, column=2)

        self.configPanelFr2 = Frame(self.configPanel)
        self.configPanelFr2.grid(row=2, column=1)
        self.configPanelFr2InfoTipsLabel = Label(self.configPanelFr2, text="备份说明信息")
        self.configPanelFr2InfoTipsLabel.grid(row=1, column=1)
        self.configPanelFr2InfoTipsText = Text(self.configPanelFr2, width=50, height=10)
        self.configPanelFr2InfoTipsText.grid(row=2, column=1)

        self.configPanelFr2infoOperatorName = Label(self.configPanelFr2, text="备份人姓名:")
        self.configPanelFr2Operator = StringVar()
        self.configPanelFr2Operator.set("xxxxxx")
        self.configPanelFr2OperatorEntry = Entry(
            self.configPanelFr2, textvariable=self.configPanelFr2Operator
        )
        self.configPanelFr2BackupBtn = Button(
            self.configPanelFr2, text="备份配置数据", command=lambda: self.backupConfig()
        )
        self.configPanelFr2BackupBtn.grid(row=3, column=1)
        pass

    def backupConfig(self):
        askyesno = tkinter.messagebox.askyesno("确认", "确认要备份配置吗?")
        if askyesno is False:
            return
        nowConfigs = self.MASTER.testmodelDict

        # 当前的模块设定数据
        curConfig = nowConfigs[self.PosId]
        dev_logger.debug(curConfig)
        # 发送报文id
        curConfig.MsgId = self.idIntVar.get()
        # 接收反馈报文id
        curConfig.rcevId = self.idRcevIntvar.get()

        tempTestDatas = []
        # 通过key索引把原来改过的内容更新回去到设置里.
        for key, value in self.testDataDict.items():
            unittest = UnitTestDataStructure(
                value.name,
                self.sendDataStringDict[key].get(),
                self.rcevDataStringDict[key].get(),
            )
            tempTestDatas.append(unittest)
        curConfig.testDatas = tempTestDatas

        configList = []
        for testDT in nowConfigs.values():
            dictData = testDT.toDict()
            configList.append(dictData)

        # 设定data数据
        self.MASTER.configJson["data"] = configList

        # 设定info数据
        self.MASTER.configJson["info"]["operator"] = self.configPanelFr2Operator.get()
        self.MASTER.configJson["info"]["Tips"] = self.configPanelFr2InfoTipsText.get(
            "1.0", "end"
        )

        with open("backupConfig.json", mode="w", encoding="utf-8") as f:
            json.dump(self.MASTER.configJson, f, ensure_ascii=False)

        pass

    def sendControlMsg(self, key):
        """
        执行具体单个测试的函数闭包
        """

        def wrapper():

            # @pysnooper.snoop('./logs/pysnooper.log')
            # @pysnooper.snoop('./logs/pysnooper.log',thread_info=True,prefix='snoop1  ',watch=[],depth=1)
            def _sendMsg():

                strData = self.testDatas[key].send
                data = strData
                # dev_logger.debug(f"待发送的信息内容为:{data}")
                record_logger.debug(f"待发送的信息内容为:{data}")

                # 把接收缓存清理一下,否则可能运行时recvMsgDeque里面收到很多原先缓存中的数据
                self.MASTER.clearBuffer1()
                # 发送消息前情况消息队列,后面就只查看队列中有没有预期的返回信息即可
                self.MASTER.recvMsgDeque.clear()
                # 发送消息
                res = self.MASTER.sendcan1_manual(self.idIntVar.get(), data_string=data)
                if res == "error":
                    self.sendDataLabelDict[key].configure(text="发送NG", background="red")
                    record_logger.debug(f'测试信号未发送成功')
                    prod_logger.error('未成功发送')
                    return "未成功发送"
                # 发送成功返回发送的数据条数
                elif res == 1:
                    self.sendDataLabelDict[key].configure(text="发送OK", background="green")
                    # 正确发送后, 从总线读取响应信息
                    # 读取之前已经把信息栈清空,然后再经过一段时间判断,收到的信息里有没有预期的数据
                    recvResult = _checkRecvMsg()
                    if recvResult is True:
                        self.recvDataLabelDict[key].configure(
                            text="返回OK", background="green"
                        )
                        record_logger.debug('数据校验正确')
                        return "数据校验正确"
                    else:
                        self.recvDataLabelDict[key].configure(text="返回NG", background="red")
                        prod_logger.error('数据校验失败')
                        record_logger.debug('数据校验失败')
                        return '数据校验失败'
                else:
                    prod_logger.error('发送数据有误')
                    record_logger.debug('发送数据有误')
                    return '发送数据有误'


            def _checkRecvMsg():

                # 读取预期的返回数据11XXXXXXXXXX
                hopeResult = self.testDatas[key].rcev
                record_logger.debug(f'预期返回的信号是：{hopeResult}')

                # FIXME: 根据实际情况设定等待信息反馈的时间,当前的线程和接收帧数据的线程不是同一个，
                # 所以当前线程休眠时，接收数据的线程继续工作，如果sleep时间过长，就会导致队列中数据太多
                # 如果sleep时间太短，队列中又可能没有收到数据
                time.sleep(1)
                # 此处把队列中的数据全部取出来，前面发送之前已经清空过一次，这时候取出来就是这个时段收到的数据
                with self.MASTER.recvMsgDequeLock:
                    # 后期考虑将列表转换为生成器
                    # gen = (msg for msg in self.MASTER.recvMsgDeque)
                    recvMsgs = list(self.MASTER.recvMsgDeque)
                msgCount = len(recvMsgs)
                record_logger.debug(f'发送信号后，回收到{msgCount}条符合id滤波的数据')
                if msgCount < 1:
                    record_logger.debug('未收到id符合的数据')
                    return False
                
                # 只有收到数据才会做for循环遍历
                for msg in recvMsgs:
                    if hex(self.rcevId) != msg.id:
                        record_logger.debug(str(msg) + '----id不匹配')
                        continue
                    else:
                        dataLength = len(hopeResult)
                        for i in range(dataLength):
                            # 设置值不是X，同时也不相等，就跳出去继续下一个

                            if hopeResult[i] != 'X' and hopeResult[i] != msg.data[i]:
                                # TODO: 此处的i值是不能马上显示的,最后才统一显示,所以实际上最后只显示了最后一条i值
                                record_logger.debug(str(msg.data) + f'<<第{i}条msg--不匹配--hope>>' + str(hopeResult) )
                                break
                        # 全部都能匹配上，返回True
                        else:
                            record_logger.debug(str(msg.data) + f'<<msg--匹配--hope>>' + str(hopeResult) )
                            return True

                # 全部都遍历完了，还没有符合条件的，返回False
                else:
                    record_logger.debug('未回收到预期的反馈信号')
                    return False

                # TODO:上面对收到的信息进判断,如果达到预期,就返回正确信息
                # if True:
                    # return True

            return self.testSinglePoolUnitKey.submit(_sendMsg)

        return wrapper

    def allTest(self):
        """
        左前座椅(主驾驶)
        此处用多线程,避免一个报错,把其他的都给堵塞死了
        """

        dev_logger.debug("run callback allTest....")

        def _allTest():
            dev_logger.debug(f"run _allTest....{self.testDatas}")

            # sendControlMsg 接收测试信息索引号为参数
            res = collections.defaultdict(int)
            for key, _ in enumerate(self.testDatas):
                td = self.sendControlMsg(key)()
                # td 是一个异步线程池任务的future对象，td.result()就是其任务执行后反馈的结果
                # 可能反馈的结果就是在闭包包裹的_sendMsg函数中return的内容
                tdRet = td.result()
                dev_logger.debug(str(tdRet))
                res[tdRet] += 1
            dev_logger.debug(res)
            resString = ''
            # 把测试结果用字典形式列出来
            resFlag = True
            for key,value in res.items():
                resString += f'{key}:{value}\n'
                # 只要有一个错误，结果就是错误的，需要确认
                if key != '数据校验正确':
                    resFlag = False
            # 去掉最后的一个空行
            resString = resString[:-1]
            self.alltestResMsg.configure(text=resString)
            foreground = 'green' if resFlag else 'red'
            self.alltestResMsg.configure(foreground=foreground)

            dev_logger.debug('alltest 运行结束....')
            return resFlag, resString

        # 把发送和接受的工作放到一个单独的线程里执行,避免堵塞主线程,每个的发送和接收仍然是同步的,避免self.ecan的使用冲突
        # td = threading.Thread(target=_allTest)
        # td.start()
        # 把结果返回出去，在外面就可以通过result()接收到，内部函数return的返回值
        return self.testSinglePoolUnit.submit(_allTest)


class FunctestUnit(FunctestTemplate):
    pass
